{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# TensorFlow2教程-自定义回调\n",
    "\n",
    "自定义回调是一个很好用的工具，可以在训练，评估和推理期间自定义模型的行为，包括读取/更改keras模型等。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/doit/anaconda3/lib/python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.\n",
      "  from ._conv import register_converters as _register_converters\n"
     ]
    }
   ],
   "source": [
    "from __future__ import absolute_import, division,print_function, unicode_literals\n",
    "import tensorflow as tf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1 Keras回调简介\n",
    "在Kreas中，Callback是一个python类，旨在被子类化以提供特定功能，并在训练的各阶段（包括每个batch/epoch的开始和结束），以及测试中调用一组方法。\n",
    "\n",
    "我们可以通过回调列表，传递回调方法，在训练/评估/推断的不同阶段调用回调方法。\n",
    "\n",
    "构建一个模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_model():\n",
    "    model = tf.keras.Sequential()\n",
    "    model.add(tf.keras.layers.Dense(1, activation = 'linear', input_dim = 784))\n",
    "    model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=0.1), loss='mean_squared_error', metrics=['mae'])\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "导入数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()\n",
    "x_train = x_train.reshape(60000, 784).astype('float32') / 255\n",
    "x_test = x_test.reshape(10000, 784).astype('float32') / 255"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "定义一个简单的自定义回调，以跟踪每批数据的开始和结束。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime\n",
    "\n",
    "class MyCustomCallback(tf.keras.callbacks.Callback):\n",
    "\n",
    "    def on_train_batch_begin(self, batch, logs=None):\n",
    "        print('Training: batch {} begins at {}'.format(batch, datetime.datetime.now().time()))\n",
    "\n",
    "    def on_train_batch_end(self, batch, logs=None):\n",
    "        print('Training: batch {} ends at {}'.format(batch, datetime.datetime.now().time()))\n",
    "\n",
    "    def on_test_batch_begin(self, batch, logs=None):\n",
    "        print('Evaluating: batch {} begins at {}'.format(batch, datetime.datetime.now().time()))\n",
    "\n",
    "    def on_test_batch_end(self, batch, logs=None):\n",
    "        print('Evaluating: batch {} ends at {}'.format(batch, datetime.datetime.now().time()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在训练时传入回调函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Training: batch 0 begins at 14:47:16.854006\n",
      "Training: batch 0 ends at 14:47:19.215169\n",
      "Training: batch 1 begins at 14:47:19.215812\n",
      "Training: batch 1 ends at 14:47:19.219226\n",
      "Training: batch 2 begins at 14:47:19.219737\n",
      "Training: batch 2 ends at 14:47:19.222817\n",
      "Training: batch 3 begins at 14:47:19.223307\n",
      "Training: batch 3 ends at 14:47:19.226198\n",
      "Training: batch 4 begins at 14:47:19.226991\n",
      "Training: batch 4 ends at 14:47:19.229930\n"
     ]
    }
   ],
   "source": [
    "model = get_model()\n",
    "_ = model.fit(x_train, y_train,\n",
    "          batch_size=64,\n",
    "          epochs=1,\n",
    "          steps_per_epoch=5,\n",
    "          verbose=0,\n",
    "          callbacks=[MyCustomCallback()])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.1 以下方法会调用回调函数\n",
    "fit(), fit_generator()\n",
    "训练或使用迭代数据进行训练。\n",
    "\n",
    "evaluate(), evaluate_generator()\n",
    "评估或使用迭代数据进行评估。\n",
    "\n",
    "predict(), predict_generator()\n",
    "预测或使用迭代数据进行预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Evaluating: batch 0 begins at 14:52:23.452381\n",
      "Evaluating: batch 0 ends at 14:52:23.492404\n",
      "Evaluating: batch 1 begins at 14:52:23.492663\n",
      "Evaluating: batch 1 ends at 14:52:23.493493\n",
      "Evaluating: batch 2 begins at 14:52:23.493606\n",
      "Evaluating: batch 2 ends at 14:52:23.494279\n",
      "Evaluating: batch 3 begins at 14:52:23.494388\n",
      "Evaluating: batch 3 ends at 14:52:23.495022\n",
      "Evaluating: batch 4 begins at 14:52:23.495128\n",
      "Evaluating: batch 4 ends at 14:52:23.495710\n"
     ]
    }
   ],
   "source": [
    "_ = model.evaluate(x_test, y_test, batch_size=128, verbose=0, steps=5,\n",
    "          callbacks=[MyCustomCallback()])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2 回调方法概述\n",
    "### 2.1 训练/测试/预测的常用方法\n",
    "为了进行训练，测试和预测，提供了以下方法来替代。\n",
    "\n",
    "on_(train|test|predict)_begin(self, logs=None)\n",
    "在fit/ evaluate/ predict开始时调用。\n",
    "\n",
    "on_(train|test|predict)_end(self, logs=None)\n",
    "在fit/ evaluate/ predict结束时调用。\n",
    "\n",
    "on_(train|test|predict)_batch_begin(self, batch, logs=None)\n",
    "在培训/测试/预测期间处理批次之前立即调用。在此方法中，logs是带有batch和size可用键的字典，代表当前批次号和批次大小。\n",
    "\n",
    "on_(train|test|predict)_batch_end(self, batch, logs=None)\n",
    "在培训/测试/预测批次结束时调用。在此方法中，logs是一个包含状态指标结果的字典。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 训练时特定方法\n",
    "另外，为了进行培训，提供以下内容。\n",
    "\n",
    "on_epoch_begin（self，epoch，logs = None）\n",
    "在训练期间的开始时调用。\n",
    "\n",
    "on_epoch_end（self，epoch，logs = None）\n",
    "在训练期间的末尾调用。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.3 logsdict的用法\n",
    "该logs字典包含损loss，已经每个batch和epoch的结束时的所有指标。示例包括loss和平均绝对误差。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "For batch 0, loss is   31.99.\n",
      "For batch 1, loss is  888.84.\n",
      "For batch 2, loss is   20.06.\n",
      "For batch 3, loss is    8.30.\n",
      "For batch 4, loss is   10.34.\n",
      "The average loss for epoch 0 is  191.91 and mean absolute error is    8.17.\n",
      "For batch 0, loss is    6.29.\n",
      "For batch 1, loss is    7.78.\n",
      "For batch 2, loss is    5.60.\n",
      "For batch 3, loss is    6.66.\n",
      "For batch 4, loss is    5.95.\n",
      "The average loss for epoch 1 is    6.45 and mean absolute error is    2.06.\n",
      "For batch 0, loss is    6.19.\n",
      "For batch 1, loss is    5.54.\n",
      "For batch 2, loss is    7.04.\n",
      "For batch 3, loss is   10.74.\n",
      "For batch 4, loss is   15.87.\n",
      "The average loss for epoch 2 is    9.07 and mean absolute error is    2.46.\n"
     ]
    }
   ],
   "source": [
    "class LossAndErrorPrintingCallback(tf.keras.callbacks.Callback):\n",
    "\n",
    "    def on_train_batch_end(self, batch, logs=None):\n",
    "        print('For batch {}, loss is {:7.2f}.'.format(batch, logs['loss']))\n",
    "\n",
    "    def on_test_batch_end(self, batch, logs=None):\n",
    "        print('For batch {}, loss is {:7.2f}.'.format(batch, logs['loss']))\n",
    "\n",
    "    def on_epoch_end(self, epoch, logs=None):\n",
    "        print('The average loss for epoch {} is {:7.2f} and mean absolute error is {:7.2f}.'.format(epoch, logs['loss'], logs['mae']))\n",
    "\n",
    "model = get_model()\n",
    "_ = model.fit(x_train, y_train,\n",
    "          batch_size=64,\n",
    "          steps_per_epoch=5,\n",
    "          epochs=3,\n",
    "          verbose=0,\n",
    "          callbacks=[LossAndErrorPrintingCallback()])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "同样，可以在evaluate时调用回调。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "For batch 0, loss is   23.48.\n",
      "For batch 1, loss is   20.85.\n",
      "For batch 2, loss is   22.11.\n",
      "For batch 3, loss is   24.52.\n",
      "For batch 4, loss is   26.07.\n",
      "For batch 5, loss is   23.15.\n",
      "For batch 6, loss is   23.08.\n",
      "For batch 7, loss is   21.37.\n",
      "For batch 8, loss is   22.78.\n",
      "For batch 9, loss is   22.38.\n",
      "For batch 10, loss is   24.63.\n",
      "For batch 11, loss is   26.51.\n",
      "For batch 12, loss is   23.29.\n",
      "For batch 13, loss is   25.12.\n",
      "For batch 14, loss is   25.20.\n",
      "For batch 15, loss is   22.27.\n",
      "For batch 16, loss is   26.29.\n",
      "For batch 17, loss is   25.76.\n",
      "For batch 18, loss is   24.94.\n",
      "For batch 19, loss is   23.94.\n"
     ]
    }
   ],
   "source": [
    "_ = model.evaluate(x_test, y_test, batch_size=128, verbose=0, steps=20,\n",
    "          callbacks=[LossAndErrorPrintingCallback()])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3 keras回调示例\n",
    "## 3.1 以最小的损失尽早停止\n",
    "第一个示例展示了Callback通过达到最小损失时更改属性model.stop_training（布尔值），停止Keras训练。用户可以提供一个参数patience来指定训练最终停止之前应该等待多少个时期。\n",
    "\n",
    "注：tf.keras.callbacks.EarlyStopping 提供了更完整，更通用的实现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "class EarlyStoppingAtMinLoss(tf.keras.callbacks.Callback):\n",
    "    def __init__(self, patience=0):\n",
    "        super(EarlyStoppingAtMinLoss, self).__init__()\n",
    "        self.patience = patience\n",
    "        self.best_weights = None  # loss最低时的权重\n",
    "    def on_train_begin(self, logs=None):\n",
    "        # loss不再下降时等待的轮数\n",
    "        self.wait = 0\n",
    "        # 停止时的轮数\n",
    "        self.stopped_epoch = 0\n",
    "        # 开始时的最优loss\n",
    "        self.best = np.Inf\n",
    "    \n",
    "    def on_epoch_end(self, epoch, logs=None):\n",
    "        current = logs.get('loss')\n",
    "        if np.less(current, self.best):\n",
    "            self.best = current\n",
    "            self.wait = 0\n",
    "            # 最佳权重\n",
    "            self.best_weights = self.model.get_weights()\n",
    "        else:\n",
    "            self.wait += 1\n",
    "            if self.wait >= self.patience:\n",
    "                self.stopped_epoch = epoch\n",
    "                self.model.stop_training = True\n",
    "                print('导入当前最佳模型')\n",
    "                self.model.set_weights(self.best_weights)\n",
    "    def on_train_end(self, logs=None):\n",
    "        if self.stopped_epoch > 0:\n",
    "            print('在%05d: 提前停止训练'% (self.stopped_epoch+1))\n",
    "        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "For batch 0, loss is   23.36.\n",
      "For batch 1, loss is 1043.09.\n",
      "For batch 2, loss is   25.19.\n",
      "For batch 3, loss is    8.33.\n",
      "For batch 4, loss is    6.51.\n",
      "The average loss for epoch 0 is  221.30 and mean absolute error is    8.41.\n",
      "For batch 0, loss is   10.12.\n",
      "For batch 1, loss is    6.01.\n",
      "For batch 2, loss is    7.86.\n",
      "For batch 3, loss is    6.52.\n",
      "For batch 4, loss is    5.53.\n",
      "The average loss for epoch 1 is    7.21 and mean absolute error is    2.26.\n",
      "For batch 0, loss is    4.40.\n",
      "For batch 1, loss is    4.59.\n",
      "For batch 2, loss is    5.08.\n",
      "For batch 3, loss is    4.74.\n",
      "For batch 4, loss is    6.71.\n",
      "The average loss for epoch 2 is    5.10 and mean absolute error is    1.89.\n",
      "For batch 0, loss is    5.96.\n",
      "For batch 1, loss is    4.13.\n",
      "For batch 2, loss is    5.94.\n",
      "For batch 3, loss is    7.71.\n",
      "For batch 4, loss is   16.43.\n",
      "The average loss for epoch 3 is    8.03 and mean absolute error is    2.30.\n",
      "导入当前最佳模型\n",
      "在00004: 提前停止训练\n"
     ]
    }
   ],
   "source": [
    "model = get_model()\n",
    "_ = model.fit(x_train, y_train,\n",
    "          batch_size=64,\n",
    "          steps_per_epoch=5,\n",
    "          epochs=30,\n",
    "          verbose=0,\n",
    "          callbacks=[LossAndErrorPrintingCallback(), EarlyStoppingAtMinLoss()])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 自定义学习率\n",
    "在模型训练中通常要做的一件事是随着训练轮次改变学习率。Keras后端公开了可用于设置变量的get_value API。在此示例中，我们展示了如何使用自定义的回调来动态更改学习率。\n",
    "\n",
    "注：这只是示例实现，请参见callbacks.LearningRateScheduler和keras.optimizers.schedules有关更一般的实现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "class LearningRateScheduler(tf.keras.callbacks.Callback):\n",
    "    def __init__(self, schedule):\n",
    "        super(LearningRateScheduler, self).__init__()\n",
    "        self.schedule = schedule\n",
    "        \n",
    "    def on_epoch_begin(self, epoch, logs=None):\n",
    "        if not hasattr(self.model.optimizer, 'lr'):\n",
    "            raise ValueError('Optimizer没有lr参数。')\n",
    "        # 获取当前lr\n",
    "        lr = float(tf.keras.backend.get_value(self.model.optimizer.lr))\n",
    "        # 调整lr\n",
    "        scheduled_lr = self.schedule(epoch, lr)\n",
    "        tf.keras.backend.set_value(self.model.optimizer.lr, scheduled_lr)\n",
    "        print('Epoch %05d: 学习率为%6.4f.'%(epoch, scheduled_lr))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "按轮次调整学习率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 00000: 学习率为0.1000.\n",
      "For batch 0, loss is   24.55.\n",
      "For batch 1, loss is 1134.79.\n",
      "For batch 2, loss is   26.95.\n",
      "For batch 3, loss is    8.14.\n",
      "For batch 4, loss is    6.28.\n",
      "The average loss for epoch 0 is  240.14 and mean absolute error is    8.91.\n",
      "Epoch 00001: 学习率为0.1000.\n",
      "For batch 0, loss is    7.12.\n",
      "For batch 1, loss is    7.65.\n",
      "For batch 2, loss is    5.11.\n",
      "For batch 3, loss is    5.36.\n",
      "For batch 4, loss is    5.43.\n",
      "The average loss for epoch 1 is    6.13 and mean absolute error is    2.06.\n",
      "Epoch 00002: 学习率为0.1000.\n",
      "For batch 0, loss is    5.18.\n",
      "For batch 1, loss is    5.11.\n",
      "For batch 2, loss is    5.08.\n",
      "For batch 3, loss is    4.34.\n",
      "For batch 4, loss is    4.21.\n",
      "The average loss for epoch 2 is    4.78 and mean absolute error is    1.77.\n",
      "Epoch 00003: 学习率为0.0500.\n",
      "For batch 0, loss is    5.30.\n",
      "For batch 1, loss is    3.95.\n",
      "For batch 2, loss is    4.07.\n",
      "For batch 3, loss is    3.63.\n",
      "For batch 4, loss is    4.44.\n",
      "The average loss for epoch 3 is    4.28 and mean absolute error is    1.69.\n",
      "Epoch 00004: 学习率为0.0500.\n",
      "For batch 0, loss is    3.40.\n",
      "For batch 1, loss is    7.80.\n",
      "For batch 2, loss is    4.98.\n",
      "For batch 3, loss is    3.08.\n",
      "For batch 4, loss is    4.66.\n",
      "The average loss for epoch 4 is    4.78 and mean absolute error is    1.75.\n",
      "Epoch 00005: 学习率为0.0500.\n",
      "For batch 0, loss is    3.86.\n",
      "For batch 1, loss is    6.12.\n",
      "For batch 2, loss is    4.59.\n",
      "For batch 3, loss is    3.81.\n",
      "For batch 4, loss is    5.60.\n",
      "The average loss for epoch 5 is    4.80 and mean absolute error is    1.70.\n",
      "Epoch 00006: 学习率为0.0100.\n",
      "For batch 0, loss is    5.00.\n",
      "For batch 1, loss is    4.14.\n",
      "For batch 2, loss is    3.43.\n",
      "For batch 3, loss is    3.61.\n",
      "For batch 4, loss is    3.96.\n",
      "The average loss for epoch 6 is    4.03 and mean absolute error is    1.58.\n",
      "Epoch 00007: 学习率为0.0100.\n",
      "For batch 0, loss is    5.50.\n",
      "For batch 1, loss is    4.07.\n",
      "For batch 2, loss is    4.17.\n",
      "For batch 3, loss is    3.99.\n",
      "For batch 4, loss is    3.56.\n",
      "The average loss for epoch 7 is    4.26 and mean absolute error is    1.60.\n",
      "Epoch 00008: 学习率为0.0100.\n",
      "For batch 0, loss is    3.93.\n",
      "For batch 1, loss is    3.56.\n",
      "For batch 2, loss is    3.22.\n",
      "For batch 3, loss is    3.66.\n",
      "For batch 4, loss is    4.47.\n",
      "The average loss for epoch 8 is    3.77 and mean absolute error is    1.52.\n",
      "Epoch 00009: 学习率为0.0050.\n",
      "For batch 0, loss is    4.83.\n",
      "For batch 1, loss is    4.33.\n",
      "For batch 2, loss is    3.32.\n",
      "For batch 3, loss is    4.38.\n",
      "For batch 4, loss is    3.41.\n",
      "The average loss for epoch 9 is    4.05 and mean absolute error is    1.63.\n",
      "Epoch 00010: 学习率为0.0050.\n",
      "For batch 0, loss is    3.49.\n",
      "For batch 1, loss is    3.63.\n",
      "For batch 2, loss is    3.74.\n",
      "For batch 3, loss is    3.56.\n",
      "For batch 4, loss is    4.65.\n",
      "The average loss for epoch 10 is    3.81 and mean absolute error is    1.54.\n",
      "Epoch 00011: 学习率为0.0050.\n",
      "For batch 0, loss is    3.46.\n",
      "For batch 1, loss is    4.41.\n",
      "For batch 2, loss is    2.83.\n",
      "For batch 3, loss is    5.34.\n",
      "For batch 4, loss is    3.91.\n",
      "The average loss for epoch 11 is    3.99 and mean absolute error is    1.53.\n",
      "Epoch 00012: 学习率为0.0010.\n",
      "For batch 0, loss is    4.89.\n",
      "For batch 1, loss is    3.13.\n",
      "For batch 2, loss is    3.65.\n",
      "For batch 3, loss is    3.14.\n",
      "For batch 4, loss is    2.93.\n",
      "The average loss for epoch 12 is    3.55 and mean absolute error is    1.53.\n",
      "Epoch 00013: 学习率为0.0010.\n",
      "For batch 0, loss is    3.93.\n",
      "For batch 1, loss is    3.33.\n",
      "For batch 2, loss is    3.01.\n",
      "For batch 3, loss is    5.31.\n",
      "For batch 4, loss is    3.33.\n",
      "The average loss for epoch 13 is    3.78 and mean absolute error is    1.56.\n",
      "Epoch 00014: 学习率为0.0010.\n",
      "For batch 0, loss is    4.76.\n",
      "For batch 1, loss is    3.42.\n",
      "For batch 2, loss is    3.05.\n",
      "For batch 3, loss is    4.45.\n",
      "For batch 4, loss is    4.50.\n",
      "The average loss for epoch 14 is    4.04 and mean absolute error is    1.57.\n"
     ]
    }
   ],
   "source": [
    "LR_SCHEDULE = [\n",
    "    # (epoch to start, learning rate) tuples\n",
    "    (3, 0.05), (6, 0.01), (9, 0.005), (12, 0.001)\n",
    "]\n",
    "\n",
    "def lr_schedule(epoch, lr):\n",
    "    if epoch < LR_SCHEDULE[0][0] or epoch > LR_SCHEDULE[-1][0]:\n",
    "        return lr\n",
    "    for i in range(len(LR_SCHEDULE)):\n",
    "        if epoch == LR_SCHEDULE[i][0]:\n",
    "            return LR_SCHEDULE[i][1]\n",
    "    return lr\n",
    "\n",
    "model = get_model()\n",
    "_ = model.fit(x_train, y_train,\n",
    "          batch_size=64,\n",
    "          steps_per_epoch=5,\n",
    "          epochs=15,\n",
    "          verbose=0,\n",
    "          callbacks=[LossAndErrorPrintingCallback(), LearningRateScheduler(lr_schedule)])\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
